Цель:
Проанализировать поведение пользователей приложения на основе записей в логе и результатов А/В-эксперимента.
Задачи:
Ход работы:
Подключим необходимые библиотеки.
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
from scipy import stats as st
import plotly.express as px
import plotly.graph_objs as go
import math as mth
import warnings
Считаем данные из файла с логами пользователей.
#Определим класс с цветами для использования в работе
class color:
bold = '\033[1m'
red = '\033[91m'
end = '\033[0m'
#Считаем данные из файла
data = pd.read_csv('dataset/logs_exp.csv', sep='\t')
data
| EventName | DeviceIDHash | EventTimestamp | ExpId | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 |
| ... | ... | ... | ... | ... |
| 244121 | MainScreenAppear | 4599628364049201812 | 1565212345 | 247 |
| 244122 | MainScreenAppear | 5849806612437486590 | 1565212439 | 246 |
| 244123 | MainScreenAppear | 5746969938801999050 | 1565212483 | 246 |
| 244124 | MainScreenAppear | 5746969938801999050 | 1565212498 | 246 |
| 244125 | OffersScreenAppear | 5746969938801999050 | 1565212517 | 246 |
244126 rows × 4 columns
data.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 EventName 244126 non-null object 1 DeviceIDHash 244126 non-null int64 2 EventTimestamp 244126 non-null int64 3 ExpId 244126 non-null int64 dtypes: int64(3), object(1) memory usage: 7.5+ MB
При первичном изучении данных, заметим, что:
datetime и занести в него дату события из столбца EventTimestamp, для более удобной работы, так как сейчас дата в этом столбце записана в секундах;Приведем названия столбцов к соответствующему виду.
#Переименуем столбцы
data = data.rename(columns={'EventName':'event_name', 'DeviceIDHash':'user_id',\
'EventTimestamp':'event_timestamp', 'ExpId':'group_id'})
data.head()
| event_name | user_id | event_timestamp | group_id | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 |
#Заменим тип столбца с датой события
data['event_timestamp'] = pd.to_datetime(data['event_timestamp'], unit='s')
#Добавим столбец с датой события
data['date'] = pd.to_datetime(data['event_timestamp'].dt.date)
data.head()
| event_name | user_id | event_timestamp | group_id | date | |
|---|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 2019-07-25 04:43:36 | 246 | 2019-07-25 |
| 1 | MainScreenAppear | 7416695313311560658 | 2019-07-25 11:11:42 | 246 | 2019-07-25 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 2019-07-25 11:28:47 | 248 | 2019-07-25 |
| 3 | CartScreenAppear | 3518123091307005509 | 2019-07-25 11:28:47 | 248 | 2019-07-25 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 2019-07-25 11:48:42 | 248 | 2019-07-25 |
data.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_name 244126 non-null object 1 user_id 244126 non-null int64 2 event_timestamp 244126 non-null datetime64[ns] 3 group_id 244126 non-null int64 4 date 244126 non-null datetime64[ns] dtypes: datetime64[ns](2), int64(2), object(1) memory usage: 9.3+ MB
Поменяли названия столбцов, тип столбца с датой и временем события, а также добавили новый столбец с датой события.
Проверим количество пропущенных значений в столбцах.
data.isna().sum()
event_name 0 user_id 0 event_timestamp 0 group_id 0 date 0 dtype: int64
data.duplicated().sum()
413
В таблице присутствует 413 дубликатов.
Запомним размер датафрейма в переменной, перед тем как приступим к удалению дубликатов и проведению других манипуляций.
data_len_original = len(data)
print('Размер таблицы до проведения манипуляций:', data_len_original)
Размер таблицы до проведения манипуляций: 244126
data = data.drop_duplicates().reset_index(drop=True)
data_len_after_manipulation = len(data)
print('Размер таблицы после проведения манипуляций:', data_len_after_manipulation)
print('Было удалено', round((data_len_original/data_len_after_manipulation * 100 -100), 2), '% данных')
Размер таблицы после проведения манипуляций: 243713 Было удалено 0.17 % данных
Для дальнейшей работы важно знать, что пользователи в группах не пересекаются. Проверим, есть ли пользователи, которые одновременно находятся в двух или более группах.
#Выведем уникальные названия групп
print(color.bold + 'Уникальных группы:' + color.end, data['group_id'].nunique(), ', а именно:', data['group_id'].unique())
print(color.bold + 'Уникальных события:' + color.end, data['event_name'].nunique(), ', а именно:', \
data['event_name'].unique())
Уникальных группы: 3 , а именно: [246 248 247] Уникальных события: 5 , а именно: ['MainScreenAppear' 'PaymentScreenSuccessful' 'CartScreenAppear' 'OffersScreenAppear' 'Tutorial']
#Проверяем, есть ли пользователи, находящиеся в нескольких группах одновременно
groups_by_id = data.groupby('user_id')['group_id'].nunique().reset_index()
# print(groups_by_id['group_id'])
count_index = 0
for i in groups_by_id['group_id']:
if i > 1:
print(data['user_id'])
else:
count_index +=1
if len(groups_by_id) == count_index:
print('Нет пользователей, одновременно находящихся в более чем 1 группе')
Нет пользователей, одновременно находящихся в более чем 1 группе
Проверим, сколько всего событий в логе, сколько всего пользователей в логе, сколько в среднем событий приходится на пользователя.
print(color.bold +'Всего событий в логе:'+color.end, data.shape[0])
print(color.bold +'Всего уникальных видов событий:'+color.end, data['event_name'].nunique())
print(color.bold +'Всего уникальных пользователей:'+color.end, data['user_id'].nunique())
print(color.bold +'В среднем на пользователя приходится'+color.end,\
round(data.groupby('user_id')['event_name'].count().mean()), color.bold +'события'+color.end)
Всего событий в логе: 243713 Всего уникальных видов событий: 5 Всего уникальных пользователей: 7551 В среднем на пользователя приходится 32 события
Посмотрим более наглядно на то, сколько событий совершают пользователи.
event_by_user = data.groupby('user_id')['event_name'].count().reset_index()
event_by_user.columns = ['user_id', 'event_count']
round(event_by_user['event_count'].describe(), 2)
count 7551.00 mean 32.28 std 65.15 min 1.00 25% 9.00 50% 20.00 75% 37.00 max 2307.00 Name: event_count, dtype: float64
plt.figure(figsize=(10, 5))
sns.histplot(event_by_user['event_count'], bins=50, kde=True)
plt.title('Распределение количества событий на пользователя')
plt.xlabel('Количество событий')
plt.ylabel('Количество пользователей')
plt.grid(which='major')
plt.show()
Как видно из графика и описания, в среднем на пользователя приходится 32 события, при этом медианное значение - 20 событий на пользователя. Минимальное количество событий равно 1, а максимальное количество событий равно 2307.
Определим за какой период у нас представлены данные в таблице.
warnings.filterwarnings('ignore')
data['date'].describe()
count 243713 unique 14 top 2019-08-01 00:00:00 freq 36141 first 2019-07-25 00:00:00 last 2019-08-07 00:00:00 Name: date, dtype: object
В датафрейме представлены данные за период с 2019-07-25 по 2019-08-07. Посмотрим как распределены данные по этому периоду. Является ли распределение одинаково полным за весь период.
plt.title('Распределение логов по дате и времени')
plt.xlabel('Дата')
plt.ylabel('Кол-во логов')
data['event_timestamp'].hist(bins=100, figsize=(20, 5), ec="yellow", fc="green", alpha=0.6, linewidth=2)
plt.show()
plt.title('Распределение логов по времени суток')
plt.xlabel('Дата')
plt.ylabel('Кол-во логов')
data['event_timestamp'].dt.hour.hist(bins=24, figsize=(20, 5),\
ec="yellow", fc="green", alpha=0.6, linewidth=2)
plt.xticks(range(0, 23))
plt.show()
На первом графике видно, что данных до августа практически нет, это может искажать наши последующие вычисления, поэтому лучше взять данных начиная с 2019-08-01 до 2019-08-07.
На втором графике распределения логов по часам в сутках видим, что наивысшая активность пользователей приходится на 14-15 часов, в то время как наименьшая активность происходит ночью, начиная с 21 вечера.
Исходя из всего вышесказанного, отбросим старые данные и посмотрим, много ли событий и пользователей мы потеряем.
original_logs = data.shape[0]
original_users = data['user_id'].nunique()
#Отбросим старые данные
data_new = data.query('event_timestamp >= "2019-08-01"')
changed_logs = data_new.shape[0]
changed_users = data_new['user_id'].nunique()
#Посмотрим как изменились количество событий и пользователей
print('Количество логов до корректировки:', original_logs)
print('Количество логов после корректировки:', changed_logs)
print(color.bold +'После удаления старых данных количество логов сократилось на' + color.end,\
round(original_logs/changed_logs * 100 - 100, 2), '%, то есть на', original_logs - changed_logs, 'логов\n')
print('Количество уникальных пользователей до корректировки:', original_users)
print('Количество уникальных пользователей после корректировки:', changed_users)
print(color.bold +'После удаления старых данных количество уникальных пользователей сократилось на' + color.end,\
round(original_users/changed_users * 100 - 100, 2), '%, то есть на', original_users - changed_users, 'пользователей')
Количество логов до корректировки: 243713 Количество логов после корректировки: 240887 После удаления старых данных количество логов сократилось на 1.17 %, то есть на 2826 логов Количество уникальных пользователей до корректировки: 7551 Количество уникальных пользователей после корректировки: 7534 После удаления старых данных количество уникальных пользователей сократилось на 0.23 %, то есть на 17 пользователей
Теперь проверим, что после удаления старых данных во всех трех экспериментальных группах есть пользователи.
group_table = data_new.groupby('group_id').agg({'user_id':['nunique','count']}).reset_index()
group_table.columns = ['Группа', 'Кол-во пользователей', 'Кол-во логов']
group_table
| Группа | Кол-во пользователей | Кол-во логов | |
|---|---|---|---|
| 0 | 246 | 2484 | 79302 |
| 1 | 247 | 2513 | 77022 |
| 2 | 248 | 2537 | 84563 |
Во всех трех группах количество пользователей примерно одинаковое. Количество событий наибольшее у группы 248.
Посмотрим, какие события есть в логах и как часто они встречаются.
events = data_new.groupby('event_name')['user_id'].count().reset_index().sort_values(by='user_id', ascending=False)
events.columns = ['Наименование события', 'Количество событий']
display(events)
| Наименование события | Количество событий | |
|---|---|---|
| 1 | MainScreenAppear | 117328 |
| 2 | OffersScreenAppear | 46333 |
| 0 | CartScreenAppear | 42303 |
| 3 | PaymentScreenSuccessful | 33918 |
| 4 | Tutorial | 1005 |
Построим график, чтобы визуально увидеть: насколько отличается количество событий по разным событиям.
plt.figure(figsize=(17, 6))
sns.barplot(data=events, x='Количество событий', y='Наименование события', color='blue', alpha=0.5)
for i, v in enumerate(events['Количество событий']):
plt.text(v+3000, i, str(round(v, 2)), ha = 'center', size = 12)
plt.title('Частота событий', fontsize=14)
plt.ylabel('Наименование событий', fontsize=14)
plt.xlabel('Количество событий', fontsize=14)
plt.show()
Наибольшее количество событий приходится на MainScreenAppear, а наименьшее количество событий приходится на Tutorial. OffersScreenAppear, CartScreenAppear, PaymentScreenSuccessful находятся на примерно на одном уровне по количеству событий.
Посмотрим теперь какое количество уникальных пользователей совершает события и посчитаем долю пользователей, которые совершали событие.
users = data_new.groupby('event_name')['user_id'].nunique().reset_index().sort_values(by='user_id', ascending=False)
users['part'] = round(users['user_id'] / data_new['user_id'].nunique() * 100, 2)
users.columns = ['Наименование события', 'Количество событий', 'Доля пользователей']
display(users)
| Наименование события | Количество событий | Доля пользователей | |
|---|---|---|---|
| 1 | MainScreenAppear | 7419 | 98.47 |
| 2 | OffersScreenAppear | 4593 | 60.96 |
| 0 | CartScreenAppear | 3734 | 49.56 |
| 3 | PaymentScreenSuccessful | 3539 | 46.97 |
| 4 | Tutorial | 840 | 11.15 |
Наибольшая доля пользователей увидели MainScreenAppear, а наименьшая Tutorial. Видно, что постепенно начиная с главной страницы события выстраиваются в воронку, начинается постепенный отток пользователей с каждым последующим кликом. Однако шаг Tutorial большинство пропускает. Это может быть связано с понятным интерфейсом и многие пропускают этот шаг. Также просмотр туториала не связан с совершением покупки и смотреть его необязательно.
Построим нашу получившуюся воронку.
funnel = users[users['Наименование события'] != 'Tutorial']
fig = go.Figure(go.Funnel(y=funnel['Наименование события'],
x=funnel['Количество событий'],
textposition='inside',
textinfo='value + percent previous',
textfont_size=14,
marker={"color":['#ed7953', '#fb9f3a', '#fdca26', '#f0f921']}
))
fig.update_layout(
title={'text':'Воронка событий',
'y':0.9,
'x':0.55,
'xanchor':'center',
'yanchor':'top'})
fig.show()
funnel['percent'] = round(100 + funnel['Количество событий'].pct_change().fillna(0) * 100, 2)
funnel['percent_leave'] = round(funnel['Количество событий'].pct_change().fillna(0) * 100, 2)
funnel.rename(columns={'percent':'Воронка', 'percent_leave':'Процент оттока'}, inplace=True)
funnel
| Наименование события | Количество событий | Доля пользователей | Воронка | Процент оттока | |
|---|---|---|---|---|---|
| 1 | MainScreenAppear | 7419 | 98.47 | 100.00 | 0.00 |
| 2 | OffersScreenAppear | 4593 | 60.96 | 61.91 | -38.09 |
| 0 | CartScreenAppear | 3734 | 49.56 | 81.30 | -18.70 |
| 3 | PaymentScreenSuccessful | 3539 | 46.97 | 94.78 | -5.22 |
Как видно из графика и таблицы большинство пользователей теряется после первого шага, что составляет 38.09% пользователей. Успешно оплачивают товары только 46.97% пользователей от общего числа уникальных пользователей.
Проверим сколько пользователей в каждой экспериментальной группе.
Для А/А-эксперимента есть 2 контрольные группы, для проверки корректности всех механизмов и расчетов проверим, находят ли статистические критерии разницу между выборками 246 и 247.
#Узнаем сколько пользователей в каждой экспериментальной группе
table_groups = data_new.groupby('group_id')['user_id'].nunique().reset_index()
table_groups.columns = ['Группа', 'Кол-во пользователей']
table_groups
| Группа | Кол-во пользователей | |
|---|---|---|
| 0 | 246 | 2484 |
| 1 | 247 | 2513 |
| 2 | 248 | 2537 |
funnel_by_groups = data_new.groupby(['event_name', 'group_id'])['user_id']\
.nunique().reset_index().sort_values(by=['group_id', 'user_id'], ascending=False)
funnel_by_groups = funnel_by_groups[funnel_by_groups['event_name'] != 'Tutorial']
funnel_by_groups['part'] = round(funnel_by_groups['user_id'] / data_new['user_id'].nunique() * 100, 2)
funnel_by_groups.columns = ['Наименование события', 'Группа', 'Кол-во пользователей', 'Доля пользователей']
funnel_by_groups['percent'] = round(100 + funnel_by_groups.groupby('Группа')['Кол-во пользователей']\
.pct_change().fillna(0) * 100, 2)
funnel_by_groups['percent_leave'] = round(funnel_by_groups.groupby('Группа')['Кол-во пользователей']\
.pct_change().fillna(0) * 100, 2)
funnel_by_groups.rename(columns={'percent':'Воронка', 'percent_leave':'Процент оттока'}, inplace=True)
cm = sns.light_palette("lightblue", as_cmap=True)
funnel_by_groups.style.background_gradient(cmap=cm)
| Наименование события | Группа | Кол-во пользователей | Доля пользователей | Воронка | Процент оттока | |
|---|---|---|---|---|---|---|
| 5 | MainScreenAppear | 248 | 2493 | 33.090000 | 100.000000 | 0.000000 |
| 8 | OffersScreenAppear | 248 | 1531 | 20.320000 | 61.410000 | -38.590000 |
| 2 | CartScreenAppear | 248 | 1230 | 16.330000 | 80.340000 | -19.660000 |
| 11 | PaymentScreenSuccessful | 248 | 1181 | 15.680000 | 96.020000 | -3.980000 |
| 4 | MainScreenAppear | 247 | 2476 | 32.860000 | 100.000000 | 0.000000 |
| 7 | OffersScreenAppear | 247 | 1520 | 20.180000 | 61.390000 | -38.610000 |
| 1 | CartScreenAppear | 247 | 1238 | 16.430000 | 81.450000 | -18.550000 |
| 10 | PaymentScreenSuccessful | 247 | 1158 | 15.370000 | 93.540000 | -6.460000 |
| 3 | MainScreenAppear | 246 | 2450 | 32.520000 | 100.000000 | 0.000000 |
| 6 | OffersScreenAppear | 246 | 1542 | 20.470000 | 62.940000 | -37.060000 |
| 0 | CartScreenAppear | 246 | 1266 | 16.800000 | 82.100000 | -17.900000 |
| 9 | PaymentScreenSuccessful | 246 | 1200 | 15.930000 | 94.790000 | -5.210000 |
fig = go.Figure()
fig.add_trace(go.Funnel(name='248',
y=funnel_by_groups.query('Группа == 248')['Наименование события'],
x=funnel_by_groups.query('Группа == 248')['Кол-во пользователей'],
textposition='inside',
textinfo='value + percent previous',
marker={"color":['#0d0887', '#46039f', '#7201a8', '#9c179e']}
))
fig.add_trace(go.Funnel(name='247',
y=funnel_by_groups.query('Группа == 247')['Наименование события'],
x=funnel_by_groups.query('Группа == 247')['Кол-во пользователей'],
textposition='inside',
textinfo='value + percent previous',
marker={"color":[ '#ed7953', '#fb9f3a', '#fdca26', '#f0f921']}
))
fig.add_trace(go.Funnel(name='246',
y=funnel_by_groups.query('Группа == 246')['Наименование события'],
x=funnel_by_groups.query('Группа == 246')['Кол-во пользователей'],
textposition='inside',
textinfo='value + percent previous',
marker={"color":[ '#9c179e', '#bd3786', '#d8576b', '#ed7953']}
))
fig.update_layout(
title={'text':'Воронка событий в экспериментальных группах',
'y':0.9,
'x':0.55,
'xanchor':'center',
'yanchor':'top'})
fig.show()
Количество пользователей в разных группах разнится ненамного. Проверим находят ли статистические критерии разницу между выборками групп 246 и 247, принимающих участие в А/А-эксперименте.
Необходимо будет сравнить доли пользователей по каждому событию в каждой группе, узнать отличаются ли эти доли или же мы можем утверждать, что между ними нет разницы.
Для этого сформируем гипотезы:
Нулевая: Доли уникальных пользователей, совершивших событие на этапе воронки, равны
Альтернативная: Доли уникальных пользователей, совершивших событие на этапе воронки, разные
Для данного анализа будем использовать метод Шидака для снижения вероятности ложнопозитивного результата, при множественном тестировании гипотез.
group_one = data_new[data_new['group_id'] == 246]
group_two = data_new[data_new['group_id'] == 247]
group_tree = data_new[data_new['group_id'] == 248]
group_one_two_combined = data_new[data_new['group_id'] != 248]
data_new = data_new[data_new['event_name'] != "Tutorial"]
def tests(g1, g2, event, m):
#Уровень статистической значимости
alpha = 0.05
#Используем метод Шидака
# m - кол-во сравнений для метода Шидака по формуле: 1 - (1 - alpha) ** 1/m
alpha_shidak = 1 - (1 - alpha) ** (1/m)
#Число пользователей совершивших события по группам
successes = np.array([g1[g1['event_name'] == event]['user_id'].nunique(),
g2[g2['event_name'] == event]['user_id'].nunique()])
#Общее кол-во пользователей в группах
number_of_users = np.array([g1['user_id'].nunique(),
g2['user_id'].nunique()])
#Пропорции успехов в 1 и 2 группах
p1 = successes[0] / number_of_users[0]
p2 = successes[1] / number_of_users[1]
#Пропорция успехов в комбинированном датафрейме
p_combined = (successes[0] + successes[1]) / (number_of_users[0] + number_of_users[1])
#Разница пропорций
difference = p1 - p2
#Считаем статистику в ст. отклонениях стандартного норм. распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) *\
(1 / number_of_users[0] + 1 / number_of_users[1]))
#Задаем стандартное нормальное распределение
distr = st.norm(0, 1)
#Если бы пропорции были равны, разница между ними = 0, т.к. распределение норм., вызовем метод
#cdf(), модуль abs() т.к. тест двусторонний, по этой же причине удваиваем результат
p_value = (1 - distr.cdf(abs(z_value))) * 2
print(color.bold +'Группы:' + color.end, g1['group_id'].unique(), 'и', g2['group_id'].unique())
print(color.bold +'Событие' + color.end, event)
print(color.bold +'p-значение:' + color.end, p_value)
if p_value < alpha_shidak:
print('Отвергаем нулевую гипотезу: между долями есть разница')
else:
print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными')
for event in data_new['event_name'].unique():
tests(group_one, group_two, event, 4)
print()
Группы: [246] и [247] Событие MainScreenAppear p-значение: 0.7570597232046099 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: [246] и [247] Событие OffersScreenAppear p-значение: 0.2480954578522181 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: [246] и [247] Событие CartScreenAppear p-значение: 0.22883372237997213 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: [246] и [247] Событие PaymentScreenSuccessful p-значение: 0.11456679313141849 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Мы имеем по 4 гипотезы для каждого теста: А1/А2-тест, и для А/В-тестов: А1/В, А2/В, А1+А2/В. Итого получается всего 16 проверок нулевых гипотез на одном наборе данных. Так как с каждой новой проверкой гипотезы растет групповая вероятность совершить ошибку первого рода (FWER), то есть выше вероятность, что хотя бы в одном из попарных сравнений будет зафиксирован ложнопозитивный результат (будет принята неверная гипотеза), то необходимо использовать метод Шидака для снижения вероятности ложнопозитивного результата со значением m = 4, так как m - это число тестируемых гипотез.
Различия между экспериментальными группами для А/А-теста не обнаружились, можем проверять гргуппу с измененным шрифтом по отдельности с каждой из контрольных групп.
for event in data_new['event_name'].unique():
tests(group_one, group_tree, event, 4)
print()
Группы: [246] и [248] Событие MainScreenAppear p-значение: 0.2949721933554552 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: [246] и [248] Событие OffersScreenAppear p-значение: 0.20836205402738917 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: [246] и [248] Событие CartScreenAppear p-значение: 0.07842923237520116 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: [246] и [248] Событие PaymentScreenSuccessful p-значение: 0.2122553275697796 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
for event in data_new['event_name'].unique():
tests(group_two, group_tree, event, 4)
print()
Группы: [247] и [248] Событие MainScreenAppear p-значение: 0.4587053616621515 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: [247] и [248] Событие OffersScreenAppear p-значение: 0.9197817830592261 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: [247] и [248] Событие CartScreenAppear p-значение: 0.5786197879539783 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: [247] и [248] Событие PaymentScreenSuccessful p-значение: 0.7373415053803964 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
При проверки группы с измененным шрифтом и контрольных групп также не было оснований считать доли разными.
for event in data_new['event_name'].unique():
tests(group_one_two_combined, group_tree, event, 4)
print()
Группы: [246 247] и [248] Событие MainScreenAppear p-значение: 0.29424526837179577 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: [246 247] и [248] Событие OffersScreenAppear p-значение: 0.43425549655188256 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: [246 247] и [248] Событие CartScreenAppear p-значение: 0.18175875284404386 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: [246 247] и [248] Событие PaymentScreenSuccessful p-значение: 0.6004294282308704 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Различия при сравнении группы с измененным шрифтом и объединенной контрольной группой не обнаружены. Следовательно, можно сделать вывод, что изменения шрифта в приложении не повлияло на поведение пользователей.
В данном проекте было проанализированно поведения покупателей на основе логов пользователей мобильного приложения. Также была изучена воронка продаж, по которой двигаются пользователи до совершения покупки и исследованны результаты А/В-эксперимента по изменению шрифта в приложении.
Была проведена предобработка данных, изменены названия столбцов и типы данных, также было выяснено за какой период лучше брать данные, чтобы не получить искажение результатов. В данном случае был взят период с 2019-08-01 по 2019-08-07.
Было выявлено, что всего в приложении 5 события. Событие Tutorial было самое наименее просматриваемое, всего 11% пользователей совершило данное событие. Пользователи практические не совершали данное событие в виду того, что оно является не обязательным и не необходимым для совершения покупки в приложении.
При исследовании были найдены события, на которых "отваливаются" большинсво пользователей, а именно, после первого шага оставалось только 62% пользователей, то есть 38% пользователей уходило из приложения. на последующих шагах потеря пользователей была минимальна, порядка 9% и 5%.
При анализе А/В-эксперимента было учтено поведение пользователей из 3 групп:
Согласно выдвинутой гипотезе необходимо было либо согласиться с ней и с тем, что доли пользователей по каждому событию в группах равны, либо отодвинуть ее в пользу альтернативной гипотезы о том, что доли разные.
При проведении А/В-тестов по каждому событию среди 2х контрольных групп (246 и 247), а также среди этих контрольных групп и экспериментально группы с изменением шрифта (248), не была обнаружена статистически значимая разница между долями пользователей в группах. Из чего следует сделать вывод, что изменение шрифта приложения не повлияло на поведение пользователей.